package nimbus
{
	import flash.events.EventDispatcher;
	
	import nimbus.commands.base.ICommand;
	import nimbus.commands.base.IUndoableCommand;
	import nimbus.events.UndoRedoEvent;
	
	[Event(name="commandAdded", type="nimbus.events.UndoRedoEvent")]
	[Event(name="undoExecuted", type="nimbus.events.UndoRedoEvent")]
	[Event(name="redoExecuted", type="nimbus.events.UndoRedoEvent")]
	[Event(name="historyTrimmed", type="nimbus.events.UndoRedoEvent")]
	
	/**
	 * Manages the history of executed commands and provides the ability to undo or redo command
	 * execution.  In order for a command to be managed for undo/redo, it must implement
	 * IUndoableCommand.  On an undo, the undo() method of the command is
	 * called.  On a redo, the execute() method is called.  
	 * 
	 * <p>Unlike normal execution, when an undo or redo is called and the command is an AsynCommand,
	 * the controller does not wait for the command to dispatch an ExecutionEvent before it
	 * dispatches its own ExecutionEvent. The controller also does not incorporate certain features
	 * such as user interaction blocking or processing message updating during an undo/redo.</p>
	 * 
	 * @see nimbus.commands.base.IUndoableCommand
	 * @see nimbus.Controller#undo()
	 * @see nimbus.Controller#redo()
	 * @see nimbus.Controller#undosAvailable
	 * @see nimbus.Controller#redosAvailable
	 * @see nimbus.Controller#maxUndos
	 */
	public class UndoRedoHistory extends EventDispatcher
	{
		/**
		 * Contains a history of commands that have been executed and are elegible for undo/redo.
		 * 
		 * @see #cursorPosition
		 */
		protected var history:Array = [];
		
		/**
		 * Retains the current position in the undo/redo history.  In general, this is the index
		 * at which the next command to be executed would be placed into the history array.
		 * If a user were to execute ten commands, then undo five of them, the cursor position
		 * would be 5 (0-based) and would indicate the index in the history array of the next
		 * redoable command or the index where a newly executed command would be placed.
		 * 
		 * @see #history
		 */
		protected var cursorPosition:int = 0;
		
		/**
		 * @private
		 */
		private var _maxUndos:uint = 30;
		
		/**
		 * Called after a command has been executed by the controller and adds it to the undo
		 * history.
		 * 
		 * @param command The IUndoableCommand that was executed.
		 */
		public function addCommand(command:IUndoableCommand):void
		{
			// Clips off any commands pending for redo.  In other words, if the user executed
			// ten commands and called undo for five of them, the cursorPosition indicates
			// there are five commands available for undo and five for redo.  When the user
			// executes a new command, we must remove the five commands available for redo before
			// adding our newly executed command to the undoable history.  The five commands
			// that were available for redo should no longer be available. 
			history = history.slice(0, cursorPosition);
			
			// If adding the command would make the history extend beyond the maximum number of
			// undos, clipForMaxUndos() will trim off the oldest command(s) and we will retain
			// the same cursor position.  Otherwise, we will increment the cursorPosition.
			if (!clipForMaxUndos())
			{
				cursorPosition++;
			}
			
			// Add the command to the history and let listeners know about it.
			history.push(command);
			dispatchEvent(new UndoRedoEvent(UndoRedoEvent.COMMAND_ADDED, [command]));
		}
		
		/**
		 * Determines the next undoable command and performs an undo operation.
		 * 
		 * @see nimbus.commands.base.IUndoableCommand#undo()
		 */
		public function undo():IUndoableCommand
		{
			if (undosAvailable > 0)
			{
				var command:IUndoableCommand = IUndoableCommand(history[cursorPosition - 1]);
				command.undo();
				cursorPosition--;
				dispatchEvent(new UndoRedoEvent(UndoRedoEvent.UNDO_EXECUTED, [command]));
				return command;
			}
			return null;
		}
		
		/**
		 * Determines the next redoable command and performs a redo operation.
		 * 
		 * @see nimbus.commands.base.ICommand#execute()
		 */
		public function redo():IUndoableCommand
		{
			if (redosAvailable > 0)
			{
				var command:IUndoableCommand = IUndoableCommand(history[cursorPosition]);
				command.execute();
				if (!clipForMaxUndos())
				{
					cursorPosition++;
				}
				dispatchEvent(new UndoRedoEvent(UndoRedoEvent.REDO_EXECUTED, [command]));
				return command;
			}
			return null;
		}
		
		/**
		 * Clears all history (both undo and redo) of commands executed.
		 */
		public function clearHistory():void
		{
			var commands:Array = history;
			history = [];
			cursorPosition = 0;
			dispatchEvent(new UndoRedoEvent(UndoRedoEvent.HISTORY_TRIMMED, commands));
		}
		
		/**
		 * The number of commands currently available for undo.
		 * 
		 * @default 0
		 */
		public function get undosAvailable():uint
		{
			return cursorPosition;
		}
		
		/**
		 * The number of commands currently available for redo.
		 * 
		 * @default 0
		 */
		public function get redosAvailable():uint
		{
			return history.length - cursorPosition;
		}
		
		/**
		 * The maximum number of undoable commands that will be stored in the history.
		 * 
		 * @default 30
		 */
		public function get maxUndos():uint
		{
			return _maxUndos;
		}
		
		/**
		 * @private
		 */
		public function set maxUndos(value:uint):void
		{
			_maxUndos = value;
			
			// If there are already more undos available than the new maxUndos value,
			// trim all the old ones that won't make the cut.  Notice that all commands available
			// for redo (located after cursorPosition) will still remain available for redo.
			if (undosAvailable > value)
			{
				var trimmedCommands:Array = history.slice(0, cursorPosition - value); 
				history = history.slice(cursorPosition - value);
				cursorPosition = value;
				dispatchEvent(new UndoRedoEvent(UndoRedoEvent.HISTORY_TRIMMED, trimmedCommands));
			}
		}
		
		/**
		 * If the undos stores is stretched to the max (in other words, if one more command execution
		 * were to take place right now too many commands would be stored for undo), this
		 * attempts to cut off the oldest one.  This is called by functions that execute a command
		 * and attempt to cut off the oldest undo command if it's going to be necessary.  This
		 * allows calling functions to determine if the cursor position will need to be changed.
		 * 
		 * @return Boolean representing whether a command was clipped.
		 */
		protected function clipForMaxUndos():Boolean
		{
			if (maxUndos > 0 && cursorPosition == maxUndos)
			{
				var trimmedCommand:IUndoableCommand = history[0];
				history = history.slice(1);
				dispatchEvent(new UndoRedoEvent(UndoRedoEvent.HISTORY_TRIMMED, [trimmedCommand]));
				return true;
			}
			return false;
		}
		
		/**
		 * Return the next undoable command.
		 */
		public function peekUndo():ICommand
		{
			if (undosAvailable > 0)
			{
				return history[cursorPosition - 1];
			}
			return null;
		}
		
		/**
		 * Return the next redoable command.
		 */
		public function peekRedo():ICommand
		{
			if (redosAvailable > 0)
			{
				return history[cursorPosition];
			}
			return null;
		}
	}
}